#!/usr/bin/env python

"""
Experimental implementation of a gap buffer using the mmap module.

http://docs.python.org/lib/module-mmap.html

It passes -1 as a fileno arg to mmap.mmap(), a feature introduced in python2.5 
only.

Important Issues:
- Necessary to have a clearer distinction between positions with gap and
  positions without.

Minor Issues:
- Is it more confortable to keep the list of slices, or the list of gap(s)?
- It would be interesting, to use two coordinate systems: "with gap" and 
  "without gap", and have functions to convert between the two.
"""

import mmap
import re
import sys


def intersect(a, b):
    if a[0] < b[1] and a[1] > b[0]:
        mn = max(a[0], b[0])
        mx = min(a[1], b[1])
        return [mn, mx, mx - mn]


class GapSlice(object):

    def __init__(self, pos, size):
        self.fr = pos
        self.to = pos + size
    
    def __len__(self):
        return self.to - self.fr

    def intersect(self, fr, to):
        if self.fr < to and fr < self.to:
            mn = max(self.fr, fr)
            mx = min(self.to, to)
            return [mn, mx, mx - mn]



class GapBuffer(object):

    def __init__(self, data='', cursor=0, maxsize=2048, gapsize=150):
        
        self.gapsize = gapsize 
        self.maxsize = max(maxsize, len(data) + gapsize) 
        self.cursor  = min(cursor, len(data))
        
        # Create the memory map (aka allocate)
        self.mmap = mmap.mmap(-1, self.maxsize)
        
        # Slice the data into two: before (a) and after (z) the cursor
        a = data[:self.cursor]
        z = data[self.cursor:]
        
        # Buffer offset of each slice
        apos = 0
        zpos = self.cursor + self.gapsize

        # Copy the two slices onto the buffer
        self._write_at(apos, a)
        self._write_at(zpos, z)
        
        # Save the positions of both slices
        self.slices = [GapSlice(apos, len(a)), GapSlice(zpos, len(z))]


    def _write_at(self, pos, data):
        self.mmap.seek(pos)
        self.mmap.write(data)


    def _read_at(self, pos, n):
        self.mmap.seek(pos)
        return self.mmap.read(n)


    def tostr(self):
        result = ''
        for sli in self.slices:
            result += self._read_at(sli.fr, len(sli))
        return result


    def _erase(self, pos, count, code=0):
        self.mmap.seek(pos)
        self.mmap.write(chr(code) * count)


    def _move(self, dest, src, count):
        self.mmap.move(dest, src, count)
        if dest > src:
            self._erase(src, dest - src) #, code=ord('>'))
        else:
            off = max(src, dest + count)
            dif = min(src - dest, count)
            self._erase(off, dif) # , code=ord('<'))


    def _widen_gap(self, inc):
        # TODO: Test for maxsize before increasing gap

        sli = self.slices[1]
        self._move(sli.fr + inc, sli.fr, len(sli))

        sli.fr += inc
        sli.to += inc


    def _getslice(self, sli):
        i = sli.start or 0
        j = sli.stop or sys.maxint
        l = len(self)
        
        # Negative slices
        if i < 0: i = l + i
        if j < 0: j = l + j
        if i >= j or i < 0 or j < 0 or i >= l or j >= l:
            return ''
        
        result = ''
        gaps = self._gaps()
        for ix in xrange(len(gaps)):
            inter = self.slices[ix].intersect(i + gaps[ix], j + gaps[ix])
            if inter:
                result += self._read_at(inter[0], inter[2])
        return result


    def _gaps(self):
        # TODO: Make this one less manual
        return [0, self.slices[1].fr - self.slices[0].to]


    def _getitem(self, pos):
        gaps = self._gaps()
        for ix in xrange(len(self.slices)):
            pos += gaps[ix]
            if pos < self.slices[ix].to:
                return self._read_at(pos, 1)


    def __getitem__(self, key):
        if isinstance(key, slice):
            return self._getslice(key)
        else:
            return self._getitem(key)


    def __len__(self):
        return sum(map(len, self.slices))


    def cursor_to(self, pos):
        """Move the cursor to a different position."""
        # TODO: Test if new cursor position is valid.
        assert 0 <= pos <= len(self)

        # Move data around gap
        count = pos - self.slices[0].to
        if   count < 0: self._move(self.slices[1].fr + count, pos, abs(count))
        elif count > 0: self._move(self.slices[0].to, self.slices[1].fr, count)
        self.slices[0].to += count
        self.slices[1].fr += count

        # Reposition cursor.
        self.cursor = pos


    def write(self, data):
        """Write data into buffer starting at current cursor position."""

        # Is it necessary to widen gap?
        if self.slices[0].to + len(data) >= self.slices[1].fr:
            self._widen_gap(len(data) + self.gapsize)

        # Write the data into the gap
        self._write_at(self.cursor, data)
        self.cursor = self.mmap.tell()
        self.slices[0].to = self.cursor


    def pprint(self, title):
        print "buff.%s" % title
        # print self.slices

        buff.mmap.seek(0)
        for i in xrange(4):
            s = buff.mmap.read(100)
            s = s.replace('\x00', '-')
            print "%5s %s" % (i, s)
            # print [ord(c) for c in s]
        print ""



if __name__ == '__main__':
    txt = "Every true programmer eventually makes his own editor. =P"
    buff = GapBuffer(txt)
    res = buff.tostr()
    buff.pprint(title="_")

    s = "[Insert some text at the beginning]"
    buff.write(s)
    buff.pprint(title='write("%s")' % s)
    
    for i in xrange(17):
        buff.write("0123456789")
    buff.pprint(title='17 * write("0123456789")')

    buff.cursor_to(15)
    buff.pprint(title="cursor_to(15)")

    buff.cursor_to(len(buff) - 10)
    buff.pprint(title='cursor_to(len(buff) - 10)')

    txt = '[Insert some text near the end]'
    buff.write(txt)
    buff.pprint(title='write("%s")' % txt)

    print 'buff[000:050]', buff[:50]
    print 'buff[250:300]', buff[250:-200]
    print 'buff[250:300]', buff[-10:-20]

